1 Описание проекта
2 Загрузка и предобработка данных
3 Исследовательский анализ данных
4 Анализ воронки событий
5 Анализ результатов А/А - теста
5.1 Группа 246
5.2 Группа 247
5.3 Z-тест
5.4 Популярное событие
5.5 Анализ А/В - тестов
5.5.1 Анализ 246/248
5.5.2 Анализ 247/248
5.5.3 Анализ 246/248
5.3 Тесты с поправкой Бонферрони
5.3.1 Анализ 246/247
5.3.2 Анализ 246/248
5.3.3 Анализ 247/248
5.3.4 Анализ 246+247/248
6 Вывод
Вы работаете в стартапе, который продаёт продукты питания. Нужно разобраться, как ведут себя пользователи вашего мобильного приложения.
Цель исследования:
Описание данных:
EventName — название события;
DeviceIDHash — уникальный идентификатор пользователя;
EventTimestamp — время события;
ExpId — номер эксперимента:
#Импортируем необходимые библиотеки
import pandas as pd
import scipy.stats as stats
import datetime as dt
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(rc={'figure.figsize':(15, 10)})
import warnings
warnings.filterwarnings('ignore')
from plotly import graph_objects as go
import plotly.express as px
import math as mth
# загрузим данне и посмотрим на них
from io import BytesIO
import requests
spreadsheet_id = '1aOfGyXcrUzkDRWSdMlcpDl-HzGQwnd-a17Y_9yGSaII'
file_name = 'https://docs.google.com/spreadsheets/d/{}/export?format=csv'.format(spreadsheet_id)
r = requests.get(file_name)
logs = pd.read_csv(BytesIO(r.content))
display(logs.head())
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
# Изучим данные
logs.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null object 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(2), object(2) memory usage: 7.5+ MB
# Проверим наличие дубликатов
logs[logs.duplicated()].count()
EventName 413 DeviceIDHash 413 EventTimestamp 413 ExpId 413 dtype: int64
В датасете 244126 записей, 4 колонки. Имена колонок необходимо привести к "хоршему стилю". Пропусков данных нет. Тип данных в колонке EventTimestamp не соответствуют содержанию колонок - изменим его на date. В датесете есть дубликаты - удалим их.
# Переименуем колонки
logs.rename(columns={'EventName':'event_name', 'DeviceIDHash':'user_id', 'EventTimestamp':'event_timestamp', 'ExpId':'exp_id'}, inplace=True)
# Удалим дубликаты
logs = logs.drop_duplicates()
logs[logs.duplicated()].count()
event_name 0 user_id 0 event_timestamp 0 exp_id 0 dtype: int64
# Изменение типа данных в колонке event_timestamp
# Cохраним дату без времени в отдельную колонку date
logs['event_timestamp'] = pd.to_datetime(logs['event_timestamp'], unit='s')
logs['date'] = logs['event_timestamp'].astype('datetime64[D]')
display(logs.head())
| event_name | user_id | event_timestamp | exp_id | date | |
|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 2019-07-25 04:43:36 | 246 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 2019-07-25 11:11:42 | 246 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 3 | CartScreenAppear | 3518123091307005509 | 2019-07-25 11:28:47 | 248 | 2019-07-25 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 2019-07-25 11:48:42 | 248 | 2019-07-25 |
Вывод Мы очистили данные - удалили дубликаты, привели дату и время к типу данных date, добавили колонку с датой без времени, привели названия колонок к правилам хорошего стиля и переименовали колонку DeviceIDHash на user_id - так как колонка хоть и хранит иждентификаторы устройств, это данные о пользователях.
# Изучим сколько событий содержит журнал событий (лог) и сколько уникальных типов событий есть
# Изучим сколько в логе даннх об уникальных пользователях
# Изучим сколько в среднем событий приходится на пользователя
# Узнаем дату первого и последнего события в журнале событий
print('Событий в журнале событий:', logs['event_name'].count(), 'типов событий:', logs['event_name'].nunique())
print('---------------------------')
print('Уникальных пользователей:', logs['user_id'].nunique())
print('---------------------------')
print('В среднем на одного уникального пользователя приходится', (logs['event_name'].count()/logs['user_id'].nunique()).round(0), 'события(-й)')
print('---------------------------')
print('Дата и время первого события - ',logs['event_timestamp'].min())
print('Дата и время последнего события - ',logs['event_timestamp'].max())
Событий в журнале событий: 243713 типов событий: 5 --------------------------- Уникальных пользователей: 7551 --------------------------- В среднем на одного уникального пользователя приходится 32.0 события(-й) --------------------------- Дата и время первого события - 2019-07-25 04:43:36 Дата и время последнего события - 2019-08-07 21:15:17
# Сгруппируем события по пользователям и посмотрим описательную статистику
users = (logs.groupby('user_id').agg({'event_name':'count'})
#.reset_index()
#.sort_values(by='event_name', ascending=False)
)
users.describe()
| event_name | |
|---|---|
| count | 7551.000000 |
| mean | 32.275593 |
| std | 65.154219 |
| min | 1.000000 |
| 25% | 9.000000 |
| 50% | 20.000000 |
| 75% | 37.000000 |
| max | 2307.000000 |
Среднее число дейстий на одного пользователя - 32.3, медианное - 20. Максимальное значений - 2307 десйтивия, это аномалия. Но, мы не можем точно сказать что все эти дейстиявям пользователя выбросы.
# Сгрупируем данные по дате
display(logs.groupby('date').agg({'event_name':'count'}))
| event_name | |
|---|---|
| date | |
| 2019-07-25 | 9 |
| 2019-07-26 | 31 |
| 2019-07-27 | 55 |
| 2019-07-28 | 105 |
| 2019-07-29 | 184 |
| 2019-07-30 | 412 |
| 2019-07-31 | 2030 |
| 2019-08-01 | 36141 |
| 2019-08-02 | 35554 |
| 2019-08-03 | 33282 |
| 2019-08-04 | 32968 |
| 2019-08-05 | 36058 |
| 2019-08-06 | 35788 |
| 2019-08-07 | 31096 |
# Постоим гистограмму по дате и времени
hist_all_time = logs['event_timestamp'].hist(bins = 100)
plt.title('Гистограмма по дате и времени', size = 14)
plt.ylabel('Частота', size = 12)
plt.xticks(rotation=45)
plt.show()
Вывод В нашем распоряжении данные с 2019-07-25 по 2019-08-07. Но, эспотенциальный рост наблюдается начиная с 1 августа 2019 года. Возможно данные до 1 августа - тест системы А/В - тестирования.
# Сделаем срез включающий наблюдения с 1 августа и позже
section_logs = logs.query('date >= "2019.08.01"')
# Постоим гистограмму по дате и времени для них
hist_section_time = section_logs['event_timestamp'].hist(bins = 100)
plt.title('Гистограмма по дате и времени', size = 14)
plt.ylabel('Частота', size = 12)
plt.xticks(rotation=45)
plt.show()
Вывод Отбросив данные до 1 августа 2019 года мы получили более ясную картину: пик суточной активности пользователей наступает примерно в середине дня и так на всего наблюдаемго периода - до 7 августа 2019 года.
# Проверим много ли событий и пользователей мы потеряли, отбросив данные до 01.08.2019
print('Событий в журнале событий:', section_logs['event_name'].count(), 'типов событий:', section_logs['event_name'].nunique())
print('Число потерянных событий:', logs['event_name'].count() - section_logs['event_name'].count())
print('Доля потерянных событий:', round(100-(section_logs['event_name'].count() / logs['event_name'].count()*100),2), '%')
print('---------------------------')
print('Уникальных пользователей:', section_logs['user_id'].nunique())
print('Число потерянных пользователей:', logs['user_id'].nunique() - section_logs['user_id'].nunique())
print('Доля потерянных пользователей:', round(100-(section_logs['user_id'].nunique() / logs['user_id'].nunique()*100),2), '%')
print('---------------------------')
print('В среднем на одного уникального пользователя приходится', (section_logs['event_name'].count()/section_logs['user_id'].nunique()).round(0), 'события(-й)')
Событий в журнале событий: 240887 типов событий: 5 Число потерянных событий: 2826 Доля потерянных событий: 1.16 % --------------------------- Уникальных пользователей: 7534 Число потерянных пользователей: 17 Доля потерянных пользователей: 0.23 % --------------------------- В среднем на одного уникального пользователя приходится 32.0 события(-й)
# Проверим вошли ли в очищенный датасет пользователи из всех трёх групп
groups = (section_logs.groupby('exp_id').agg({'user_id':'nunique'})
.reset_index()
#.sort_values(by='user_id', ascending=False)
)
display(groups)
| exp_id | user_id | |
|---|---|---|
| 0 | 246 | 2484 |
| 1 | 247 | 2513 |
| 2 | 248 | 2537 |
Вывод Группа 246 самая маленькая (2484 человека), посмотрим на сколько другие группы больше ней, и поймём критично ли это различие.
groups_1 = groups
groups_1['delta_next'] = round(groups_1['user_id']/groups_1['user_id'].shift()*100, 2)
groups_1['delta_246/248'] = round(groups_1['delta_next']+groups_1['delta_next'].shift()-100, 2)
display(groups_1)
| exp_id | user_id | delta_next | delta_246/248 | |
|---|---|---|---|---|
| 0 | 246 | 2484 | NaN | NaN |
| 1 | 247 | 2513 | 101.17 | NaN |
| 2 | 248 | 2537 | 100.96 | 102.13 |
Вывод Группа 247 больше группы 246 на 1.17%, группа 248 больше 247-й на 0.96%, группа 248 на 2.13% больше 246-й.На мой взгляд, данные различи в группах не досточно существенны, чтобы признать тест несостоятельным.
# Проверим, есть ли пользователи попавшие в несколько группах теста одновременно
# Cравним число пользователей в группах с общим числом пользователей
group_test = (section_logs.groupby('user_id').agg({'exp_id':'nunique'})
.reset_index()
)
group_test_query = group_test.query('exp_id < 2')
print('Общее число пользователей - ', group_test['exp_id'].sum(), 'человек')
print('Число пользователей, попавших более чем в одну группу - ', group_test['exp_id'].sum() - group_test_query['exp_id'].sum(), 'человек')
Общее число пользователей - 7534 человек Число пользователей, попавших более чем в одну группу - 0 человек
Выводы:
# Посмотрим, какие события есть в журнале событий и частоту (количество) каждого из них
funnel = (section_logs.groupby('event_name').agg({'event_name':'count'})
.rename(columns=({'event_name':'count'}))
.reset_index()
.sort_values(by='count', ascending=False)
)
display(funnel)
| event_name | count | |
|---|---|---|
| 1 | MainScreenAppear | 117328 |
| 2 | OffersScreenAppear | 46333 |
| 0 | CartScreenAppear | 42303 |
| 3 | PaymentScreenSuccessful | 33918 |
| 4 | Tutorial | 1005 |
Описание событий:
# Посчитаем, сколько пользователей совершали каждое из событий
funnel_users = (section_logs.groupby('event_name').agg({'user_id':'nunique',})
.reset_index()
.sort_values(by='user_id', ascending=False)
)
funnel_users['ratio, %'] = (funnel_users['user_id']/section_logs['user_id'].nunique()*100).round(1)
display(funnel_users)
| event_name | user_id | ratio, % | |
|---|---|---|---|
| 1 | MainScreenAppear | 7419 | 98.5 |
| 2 | OffersScreenAppear | 4593 | 61.0 |
| 0 | CartScreenAppear | 3734 | 49.6 |
| 3 | PaymentScreenSuccessful | 3539 | 47.0 |
| 4 | Tutorial | 840 | 11.1 |
Вывод доля пользователй посмотревших:
Правильный порядок совершений действия пользователями:
По воронке событий посчитаем, какая доля пользователей проходит на следующий шаг воронки.
Уберем из датафрейма Tutorial - его открывают не так часто, и он неукладывается в воронку.
# Расчитаем долю польователей перешедших на следущий шаг (конверсию) исключив событие Tutorial
funnel_users_nt = funnel_users.query('event_name != "Tutorial"')
funnel_users_nt['conversion'] = (funnel_users_nt['user_id']/funnel_users_nt['user_id'].shift(fill_value = 0)*100).round()
display(funnel_users_nt)
| event_name | user_id | ratio, % | conversion | |
|---|---|---|---|---|
| 1 | MainScreenAppear | 7419 | 98.5 | inf |
| 2 | OffersScreenAppear | 4593 | 61.0 | 62.0 |
| 0 | CartScreenAppear | 3734 | 49.6 | 81.0 |
| 3 | PaymentScreenSuccessful | 3539 | 47.0 | 95.0 |
# Построим график-воронку
fig = go.Figure()
fig = go.Figure(go.Funnel(
y = funnel_users_nt['event_name'],
x = funnel_users_nt['user_id'],
textinfo = "value+percent previous",
hoverinfo = "percent initial+percent previous")
)
fig.update_layout(title="Число пользователей, перешедших на следующий шаг воронки")
fig.show()
print('До оплаты доходят', ((funnel_users_nt.loc[3,'user_id'] / funnel_users_nt.loc[1,'user_id'])*100).round(1), \
'% первоначальных пользователей')
До оплаты доходят 47.7 % первоначальных пользователей
Выводы:
# Посмортим (ещё раз) сколько пользователей попало в каждую из групп
display(groups)
| exp_id | user_id | delta_next | delta_246/248 | |
|---|---|---|---|---|
| 0 | 246 | 2484 | NaN | NaN |
| 1 | 247 | 2513 | 101.17 | NaN |
| 2 | 248 | 2537 | 100.96 | 102.13 |
Проверим, находят ли статистические критерии разницу между выборками 246 и 247.
Для этого построим воронку для каждой тестовой группы и сравним шаги этих воронок - доли пользователей на каждом из шагов - с помощью z-теста.
# Данные каждой группы сохраним в отделньй датасет и исключим щаг Tutorial
exp_246 = section_logs.query('exp_id == 246 & event_name != "Tutorial"')
exp_247 = section_logs.query('exp_id == 247 & event_name != "Tutorial"')
exp_248 = section_logs.query('exp_id == 247 & event_name != "Tutorial"')
# Посчитаем конверсию в группе 246 на каждом из шагов
exp_246_group = (exp_246.groupby('event_name').agg({'user_id':'nunique'})
.reset_index()
.rename(columns=({'user_id':'count'}))
.sort_values(by='count', ascending=False)
)
display(exp_246_group)
| event_name | count | |
|---|---|---|
| 1 | MainScreenAppear | 2450 |
| 2 | OffersScreenAppear | 1542 |
| 0 | CartScreenAppear | 1266 |
| 3 | PaymentScreenSuccessful | 1200 |
# Построим график-воронку
fig = go.Figure()
fig.add_trace(go.Funnel(
y = exp_246_group['event_name'],
x = exp_246_group['count'],
textinfo = "value+percent initial",
hoverinfo = "percent initial+percent previous")
)
fig.update_layout(title="Группа 246 - Число пользователей на каждом шаге воронки")
fig.show()
# Посчитаем конверсию в группе 247 на каждом из шагов
exp_247_group = (exp_247.groupby('event_name').agg({'user_id':'nunique'})
.reset_index()
.rename(columns=({'user_id':'count'}))
.sort_values(by='count', ascending=False)
)
display(exp_247_group)
| event_name | count | |
|---|---|---|
| 1 | MainScreenAppear | 2476 |
| 2 | OffersScreenAppear | 1520 |
| 0 | CartScreenAppear | 1238 |
| 3 | PaymentScreenSuccessful | 1158 |
# Построим график-воронку
fig = go.Figure()
fig.add_trace(go.Funnel(
y = exp_247_group['event_name'],
x = exp_247_group['count'],
textinfo = "value+percent initial",
hoverinfo = "percent initial+percent previous")
)
fig.update_layout(title="Группа 247 - Число пользователей на каждом шаге воронки")
fig.show()
Вывод На первый взгляд распеределение пользователей по шагам воронки в группах 246 и 247 одинакове, но проверим это z-тестом.
# Расчитаем количество пользователей на каждом из шагов по группам
funnel_z = (section_logs.pivot_table(index = 'event_name', columns='exp_id', values='user_id', aggfunc='nunique')
.reset_index()
.query('event_name != "Tutorial"')
)
funnel_z['total'] = funnel_z[246]+funnel_z[247]+funnel_z[248]
funnel_z = funnel_z.sort_values(by='total', ascending=False)
display(funnel_z)
| exp_id | event_name | 246 | 247 | 248 | total |
|---|---|---|---|---|---|
| 1 | MainScreenAppear | 2450 | 2476 | 2493 | 7419 |
| 2 | OffersScreenAppear | 1542 | 1520 | 1531 | 4593 |
| 0 | CartScreenAppear | 1266 | 1238 | 1230 | 3734 |
| 3 | PaymentScreenSuccessful | 1200 | 1158 | 1181 | 3539 |
Сформулируем гипотезы:
Напишем функцию, которая будет принимать значения 246 (группа А) и 247 (группа А1) групп, а выдавать статистическую разницу по ним по каждому событию.
# Функция для проверки гипотезы о равенстве долей
alpha = 0.05 # Зададим уровень статистической значимости = 5%
def z_test(array, trials, column1, column2):
for i in range(0, 4):
successes = np.array([array.loc[i, column1], array.loc[i, column2]]) # пропорция успехов переходов в группе А
p1 = successes[0]/trials[0] # пропорция успехов переходов в группе А1
p2 = successes[1]/trials[1] # пропорция успехов в комбинированном датасете
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1]) # разница пропорций в датасетах
difference = p1 - p2 # считаем стандартное отклонение нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1])) # задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = stats.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('{} p-значение: {}'.format(funnel_z['event_name'][i], p_value))
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print('Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными')
print('------------')
#Сравним группы А/А1
trials = np.array([groups.loc[0, 'user_id'], groups.loc[1, 'user_id']])
z_test(funnel_z, trials, 246, 247)
CartScreenAppear p-значение: 0.22883372237997213 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ MainScreenAppear p-значение: 0.7570597232046099 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ OffersScreenAppear p-значение: 0.2480954578522181 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ PaymentScreenSuccessful p-значение: 0.11456679313141849 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------
Вывод Статистически значимой разницы между долями пользователей на каждом из шагов воронки в группах 246 и 247 - нет.
# Построим график-воронку
fig = go.Figure()
fig.add_trace(go.Funnel(
name = '246',
y = funnel_z['event_name'],
x = funnel_z[246],
textinfo = "value+percent initial",
hoverinfo = "percent initial+percent previous"))
fig.add_trace(go.Funnel(
name = '247',
y = funnel_z['event_name'],
x = funnel_z[247],
textinfo = "value+percent initial",
hoverinfo = "percent initial+percent previous"))
fig.add_trace(go.Funnel(
name = '248',
y = funnel_z['event_name'],
x = funnel_z[248],
textinfo = "value+percent initial",
hoverinfo = "percent initial+percent previous"))
fig.update_layout(title="Группы 246, 247 и 248")
fig.show()
Вывод Самое популярное событие MainScreenAppear - показ главного экрана
# Объеденим данные групп 246 и 247
funnel_z['246+247'] = funnel_z[246]+funnel_z[247]
display(funnel_z)
| exp_id | event_name | 246 | 247 | 248 | total | 246+247 |
|---|---|---|---|---|---|---|
| 1 | MainScreenAppear | 2450 | 2476 | 2493 | 7419 | 4926 |
| 2 | OffersScreenAppear | 1542 | 1520 | 1531 | 4593 | 3062 |
| 0 | CartScreenAppear | 1266 | 1238 | 1230 | 3734 | 2504 |
| 3 | PaymentScreenSuccessful | 1200 | 1158 | 1181 | 3539 | 2358 |
# Для того чтобы в дальнешем автоматизировать построение графиков-воронок напишем функцию
def funnel (x,y):
fig = go.Figure()
fig.add_trace(go.Funnel(
name = group_1,
y = funnel_z['event_name'],
x = funnel_z[x],
textinfo = "value+percent initial",
hoverinfo = "percent initial+percent previous"))
fig.add_trace(go.Funnel(
name = group_2,
y = funnel_z['event_name'],
x = funnel_z[y],
textinfo = "value+percent initial",
hoverinfo = "percent initial+percent previous"))
fig.update_layout(title="Группы {} и {}".format(x, y))
fig.show()
# Сравниваем А/В
trials = np.array([groups.loc[0, 'user_id'], groups.loc[2, 'user_id']])
z_test(funnel_z, trials, 246, 248)
CartScreenAppear p-значение: 0.07842923237520116 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ MainScreenAppear p-значение: 0.2949721933554552 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ OffersScreenAppear p-значение: 0.20836205402738917 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ PaymentScreenSuccessful p-значение: 0.2122553275697796 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------
# Построим график-воронку
group_1 = '246'
group_2 = '248'
funnel(246, 248)
Вывод Статистически значимой разницы между долями пользователей на каждом из шагов воронки в группах 246 и 248 - нет.
# Сравниваем А1/В
trials = np.array([groups.loc[1, 'user_id'], groups.loc[2, 'user_id']])
z_test(funnel_z, trials, 247, 248)
CartScreenAppear p-значение: 0.5786197879539783 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ MainScreenAppear p-значение: 0.4587053616621515 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ OffersScreenAppear p-значение: 0.9197817830592261 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ PaymentScreenSuccessful p-значение: 0.7373415053803964 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------
# Построим график-воронку
group_1 = '247'
group_2 = '248'
funnel(247, 248)
Вывод Статистически значимой разницы между долями пользователей на каждом из шагов воронки в группах 247 и 248 - нет.
#Объеденим значений контрольных групп А и А1
i = {'exp_id':'246+247', 'user_id':2484+2513}
groups = groups.append(i, ignore_index=True)
groups
| exp_id | user_id | delta_next | delta_246/248 | |
|---|---|---|---|---|
| 0 | 246 | 2484 | NaN | NaN |
| 1 | 247 | 2513 | 101.17 | NaN |
| 2 | 248 | 2537 | 100.96 | 102.13 |
| 3 | 246+247 | 4997 | NaN | NaN |
# Сравниваем А+А1/В
trials = np.array([groups.loc[3, 'user_id'], groups.loc[2, 'user_id']])
z_test(funnel_z, trials, '246+247', 248)
CartScreenAppear p-значение: 0.18175875284404386 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ MainScreenAppear p-значение: 0.29424526837179577 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ OffersScreenAppear p-значение: 0.43425549655188256 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------ PaymentScreenSuccessful p-значение: 0.6004294282308704 Не получилось отвергнуть нулевую гипотезу: нет оснований считать доли разными ------------
# Построим график-воронку
group_1 = '246+247'
group_2 = '248'
funnel('246+247', 248)
Вывод Статистически значимой разницы между долями пользователей на каждом из шагов воронки в объеденённой группе 246+247 и группе 248 - нет.
Выводы:
Поскольку мы проводим множественный А/А/В - тест, применим применим поправку Бонферрони и проверим гипотезы с ней. Гипотезы остануться прежними.
# Напишем функция для проверки гипотезы о равенстве долей с поправкой Бонферрони
def z_test_bomferoni(array, trials, column1, column2, alpha):
for i in range(0, 4):
successes = np.array([array.loc[i, column1], array.loc[i, column2]]) # пропорция успехов переходов в группе А
p1 = successes[0]/trials[0] # пропорция успехов переходов в группе А1
p2 = successes[1]/trials[1] # пропорция успехов в А+А1
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1]) # разница пропорций в датасетах
difference = p1 - p2 # считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1])) # задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = stats.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
#alpha = 0.05/16 # критический уровень статистической значимости (с поправкой Бонферрони)
print('{} p-значение: {}'.format(funnel_z['event_name'][i], p_value))
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
print('')
alpha = 0.05/16 # Делим наш доверительный инетрвал на число тестов
#Ставниваем А/А1
trials = np.array([groups.loc[0, 'user_id'], groups.loc[1, 'user_id']])
z_test_bomferoni(funnel_z, trials, 246, 247, alpha)
CartScreenAppear p-значение: 0.22883372237997213 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.7570597232046099 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.2480954578522181 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.11456679313141849 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
#Ставниваем А/В
trials = np.array([groups.loc[0, 'user_id'], groups.loc[2, 'user_id']])
z_test_bomferoni(funnel_z, trials, 246, 248, alpha)
CartScreenAppear p-значение: 0.07842923237520116 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.2949721933554552 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.20836205402738917 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.2122553275697796 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
#Ставниваем А1/В
trials = np.array([groups.loc[1, 'user_id'], groups.loc[2, 'user_id']])
z_test_bomferoni(funnel_z, trials, 247, 248, alpha)
CartScreenAppear p-значение: 0.5786197879539783 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.4587053616621515 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.9197817830592261 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.7373415053803964 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
#Ставниваем А+А1/В
trials = np.array([groups.loc[3, 'user_id'], groups.loc[2, 'user_id']])
z_test_bomferoni(funnel_z, trials, '246+247', 248, alpha)
CartScreenAppear p-значение: 0.18175875284404386 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.29424526837179577 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.43425549655188256 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.6004294282308704 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Вывод С поправкой Бонферрони результат остаётся прежним - статистически значимой разницы между долями пользователей на каждом из шагов воронки в контрольных группах 246 и 247 и эксперементальной группе 248 - нет.
Анализ данных показал:
Анализ воронки показал, что:
Анализ результатов А/А/В – тестов
#Это был самый быстрый проект за всё обучение в Практикуме, плюс одна бессоная ночь
from IPython.display import Image
display(Image(url='https://i.gifer.com/gl6.gif', width = 300))